package com.wangsan.study.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.util.Pair;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.wangsan.study.domain.ResourceEntity;
import com.wangsan.study.domain.RoleEntity;
import com.wangsan.study.service.ResourceEntityService;

/**
 * CustomFilterInvocationSecurityMetadataSource
 *
 * @author wangsan
 * @date 2016/12/9
 */
@Component("customFilterInvocationSecurityMetadataSource")
@Transactional
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = new ConcurrentHashMap<>();

    @Autowired
    private ResourceEntityService resourceEntityService;

    /**
     * from cache
     *
     * @return
     */
    protected Map<RequestMatcher, Collection<ConfigAttribute>> getRequestMap() {
        List<Pair<String, String>> ruleList = new ArrayList<>();
        ruleList.add(Pair.of("/login", AuthenticatedVoter.IS_AUTHENTICATED_ANONYMOUSLY));
        ruleList.add(Pair.of("/logout", AuthenticatedVoter.IS_AUTHENTICATED_ANONYMOUSLY));
        ruleList.add(Pair.of("/test", "ROLE_Test"));
        ruleList.add(Pair.of("/message/*", "ROLE_Admin"));
        ruleList.add(Pair.of("/test", "ROLE_Admin"));

        // from db
        final Iterable<ResourceEntity> resourceList = resourceEntityService.findAll();
        for (ResourceEntity resourceEntity : resourceList) {
            final Set<RoleEntity> roleEntitySet = resourceEntity.getRoleEntitySet();
            for (RoleEntity roleEntity : roleEntitySet) {
                ruleList.add(Pair.of(resourceEntity.getPath(), "ROLE_" + roleEntity.getName()));
            }
        }

        ruleList.add(Pair.of("/**", AuthenticatedVoter.IS_AUTHENTICATED_FULLY));

        final Map<String, List<String>> ruleMap =
                ruleList.stream()
                        .collect(Collectors.groupingBy(
                                Pair::getFirst,
                                LinkedHashMap::new,
                                Collectors.mapping(Pair::getSecond, Collectors.toList()))
                        );

        final Map<RequestMatcher, Collection<ConfigAttribute>> requestMatcherSetMap =
                ruleMap.entrySet().stream()
                        .collect(Collectors.toMap(
                                e -> new AntPathRequestMatcher(e.getKey()),
                                e -> e.getValue().stream().map(SecurityConfig::new).collect(Collectors.toList()),
                                (u, v) -> {
                                    throw new IllegalStateException(String.format("Duplicate key %s", u));
                                },
                                LinkedHashMap::new)
                        );

        return requestMatcherSetMap;
    }

    /**
     * @param object
     *
     * @return
     *
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : getRequestMap().entrySet()) {
            if (entry.getKey().matches(request)) {
                return entry.getValue();
            }
        }

        return null;
    }

    /**
     * ??WebExpressionConfigAttribute???WebExpressionConfig?????????????????????
     * ??????ExpressionUrlAuthorizationConfigurer,???java config?????.
     * vote?WebExpressionVoter
     *
     * @param object
     *
     * @return
     *
     * @throws IllegalArgumentException
     */
    @Deprecated
    public Collection<ConfigAttribute> getAttributes4WebExpressionConfig(Object object)
            throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();

        List<Pair<String, String>> ruleList = new ArrayList<>();
        ruleList.add(Pair.of("/login", "permitAll"));
        ruleList.add(Pair.of("/resources/**", "permitAll"));
        ruleList.add(Pair.of("/test", "hasRole('ROLE_Test')"));
        ruleList.add(Pair.of("/message/*", "hasRole('ROLE_Admin')"));
        ruleList.add(Pair.of("/test", "hasRole('ROLE_Admin')"));

        // TODO test
        final Map<String, Set<String>> ruleMap =
                ruleList.stream().collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond,
                        Collectors.toSet())));

        final Map<String, String> map = ruleMap.entrySet().stream()
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().stream().collect(Collectors.joining(" or ")
                )));

        final String attributeList = Optional.ofNullable(map.get(url)).orElse("authenticated");
        return SecurityConfig.createList(attributeList);
    }

    /**
     * @return
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

}
